import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Stack;
import java.util.Random;

/**
 * A class that provides the functionality required to run a SvoiKozyri game.
 * 
 * @author Malcheck s'Palcheck tesm
 * @version 2.5
 */
class Game
{
   // Attributes
   LinkedList<Player> _players;
   Player _currentPlayer;
   Deck _deck;
   ArrayList<Player> _winOrder;
   
   // Constant Variables
   private final int MAX_PLAYERS = 4;
   private final int HUMAN_PLAYER = 1;
   private final int COMPUTER_PLAYER = 0;
   
   private final int HAND_SIZE = 9;
   
   private final int DIAMONDS = 1;
   private final int HEARTS = 2;
   private final int CLUBS = 3;
   private final int SPADES = 4;
   
   
   // Constructor
   /**
    * Creates a new Svoi Kozyri game
    */
   public Game()
   {
      // Initialize instance variables.
      _deck = new Deck();
      _players = new LinkedList<Player>();
      _winOrder = new ArrayList<Player>();
   }
   
   
   // Methods
   /**
    * Initializes the players based on how many human players are specified
    * 
    * @param numHumanPlayers The number of human players for this game
    */
   public void setupPlayers(int numHumanPlayers)
   {      
      // add the specified number of human players
      for(int i = 0; i < numHumanPlayers; i++)
      {
         _players.add(new Player(HUMAN_PLAYER, i + 1));
      }
      
      // After creating all of the human players, create computer players
      for(int i = numHumanPlayers; i < MAX_PLAYERS; i++)
      {
         // Create the new computer player
         _players.add(new Player(COMPUTER_PLAYER, i + 1));
      }
      
      // set the current player
      _currentPlayer = _players.peek();
   }
   
   
   /**
    * Sets the trumps of the computers AFTER all of the human
    * players have chosen their trumps
    */
   public void setComputerTrumps()
   {
      Stack<Integer> trumps = new Stack<Integer>();
      int humanPlayerCount = 0;
      
      for(int trump : new int[]{DIAMONDS, HEARTS, CLUBS, SPADES})
      {
         trumps.push(trump);
      }
      
      // circle around to get to the first human player
      while(!isPlayerHuman())
      {
         getNextPlayer();
      }
      
      // then record their trumps
      while(isPlayerHuman() && humanPlayerCount < 4)
      {
         trumps.remove((Integer)_currentPlayer.getTrump());
         getNextPlayer();
         humanPlayerCount++;
      }
      
      // now choose AI trumps accordingly
      while(!isPlayerHuman())
      {
         _currentPlayer.setTrump(trumps.get(0));
         trumps.remove(0);
         
         getNextPlayer();
      }
   }
   
   
   /**
    * Begins a new game of Svoi Kozyri. Every player will receive a hand, which
    * will contain the six of their trump.
    */
   public void startGame()
   {
      ArrayList<Card> theHand;
      ArrayList<Card> sixes = new ArrayList<Card>();
      
      // Shuffle the deck before dealing.
      _deck.shuffle();
      
      // For each player in the game, deal a hand of cards.
      for(int i = 0; i < _players.size(); i++)
      {
         // Request the hand to be dealt to the player.
         theHand = _deck.deal(HAND_SIZE);
         
         // Remove any sixes present in the hand, and place them in a
         // pool, to be used later to give each player their own six.
         for(int j = 0; j < theHand.size(); j++)
         {
            // If the current card is a six, add it to the pool of sixes and
            // remove it from the hand.
            if(theHand.get(j).getDenomination() == 6)
            {
               sixes.add(theHand.get(j));
               theHand.remove(j);
               j--;
            }
         }
         
         // Give the hand to the player.
         _currentPlayer.setHand(theHand);
         getNextPlayer();
      }
      
      // Give each six in the pool of sixes to the appropriate player.
      for(int i = 0; i < _players.size(); i++)
      {
         // Find and place the six that matches the current player's trump
         for(int j = 0; j < sixes.size(); j++)
         {
            // If the suit of the current card matches the suit of the current
            // player, add the card to the player's hand.
            if(sixes.get(j).getSuit() == _currentPlayer.getTrump())
            {
               _currentPlayer.add(sixes.get(j));
            }
         }
         
         getNextPlayer();
      }
   }
   
   
   /**
    * Plays a round for the current computer player.
    * Plays cards if possible, otherwise draws cards.
    * 
    * @throws IllegalStateException If current player is not a computer.
    */
   public void playComputerRound() 
         throws IllegalArgumentException, IllegalStateException
   {
      ArrayList<Card> hand = _currentPlayer.getHand();
      Card highCard = null;
      Card trumpCard = null;
      Card[] cardsToPlay = new Card[2];
      Card top;
      boolean sameCard = true;

      // This is a computer-player-only operation
      if(isPlayerHuman())
      {
         throw new IllegalStateException("Method playComputerRound requires current player to be a computer.");
      }
      // The game should not play a round if the current player has no cards.
      else if(!playerHasCards())
      {
         // Move on to the next player before throwing an exception.
         getNextPlayer();
         
         throw new IllegalArgumentException("The current player has no cards.");
      }
      
      // Determine whether the player has a card that is able to beat the top 
      // card of the deck. Choosing the card if it exists, drawing if it
      // doesn't. 
      // 
      // The player has a card that is able to beat the top card if:
      // + The player has a card of their trump, and the top card is either
      // not the trump or a trump lower than the trump card in the hand
      // + The player has a card of same suit and higher than the top card
      
      // If the deck is not empty, gets the top card and determines valid
      // cards to play
      if (!_deck.isEmpty())
      {
          // Retrieves the top card of the deck
          top = _deck.getTop();
          
          // if there's only one card, play it
          // If they can't play it and the deck is empty, pass
          if(hand.size() == 1)
          {
             if(!playSingleCard(hand.get(0)))
             {
                drawCards();
             }
             
          }
          else
          {
             // Find two possible valid cards:
             // the lowest valid card in the player's hand with the same suit 
             // as the top card and the lowest valid trump card.
             for(int i = 0; i < hand.size(); i++)
             {
                // Find the lowest trump card. Playable if the top card's suit is
                // not the trump.
                if(top.getSuit() != _currentPlayer.getTrump() && 
                   hand.get(i).getSuit() == _currentPlayer.getTrump() &&
                   (trumpCard == null || 
                    trumpCard.getDenomination() > hand.get(i).getDenomination()))
                {                
                    trumpCard = hand.get(i);
                }
                
                // If the card matches the suit of the the top card,
                // checks if the current card is higher than the top card and 
                // lower then the previously stored 
                // high card (if one exists). Saves the card.
                else if (hand.get(i).getSuit() == top.getSuit() && 
                         hand.get(i).getDenomination() > top.getDenomination() &&
                        (highCard == null || 
                         hand.get(i).getDenomination() < highCard.getDenomination()))
                {
                    highCard = hand.get(i);
                }
             }
             
             // Determine if one of the two chosen cards is able to beat the top card
             // or if the player needs to draw.
             if(!(highCard == null && trumpCard == null))
             {
                 // If there is no valid high card to beat the top card,
                 // chooses any trumpCard
                 if (highCard == null)
                 {
                     cardsToPlay[0] = trumpCard;
                 }
                 // Otherwise, plays the high card
                 else
                 {
                     cardsToPlay[0] = highCard;
                 }
                 
                 // Plays random card on top of the beating card if the
                 // player still has cards in their hand. This random card cannot
                 // be the same as the beating card.
                 if(_currentPlayer.getSize() > 1)
                 {
                     while (sameCard)
                     {
                         cardsToPlay[1] = getRandomCard();
                         if (cardsToPlay[1] != cardsToPlay[0])
                         {
                             sameCard = false;
                         }
                     }
                     
                     // Attempts to play the cards
                     if (!playCards(cardsToPlay))
                     {
                         throw new IllegalArgumentException();
                     }
                 }
                 // If there are no other cards, only plays the beating card.
                 else
                 {
                     playCards(cardsToPlay);
                 }
             }
             // If there is neither a high card or a trump card that will beat
             // the top card, the computer must draw cards instead.
             else
             {
                 drawCards();
             }
         }
      }
      // When the deck is empty, plays a single (random) card from the hand
      else
      {
          playSingleCard(getRandomCard());
      }
   }
   
   
   /**
    * @return a random card in a player's hand
    */
   private Card getRandomCard()
   {
       Random rand = new Random();
       int k = rand.nextInt(_currentPlayer.getHand().size());
       Card playCard = _currentPlayer.getHand().get(k);
       return playCard;
   }
   
   
   /**
    * Returns a boolean value indicating if the given card
    * can be played/beats the current top card
    * 
    * @param beatCard card that will be played
    * @return True if it's valid, false if not
    */
   public boolean beatsTop(Card beatCard)
   {
      boolean valid = false;
      Card top;
      
      // if the deck is empty, then any card can be played
      if(_deck.isEmpty())
      {
         valid = true;
      }
      else
      {
         top = _deck.getTop();
         
         // If the suit of the card matches the suit of the top card
         // AND the denomination of the card is greater than the denomination
         // of the top card
         if (beatCard.getSuit() == top.getSuit() && 
             beatCard.getDenomination() > top.getDenomination())
         {
            valid = true;
         }
         // If the suit of the card is a player's trump card, 
         // AND the top card is either a non-trump card
         else if(beatCard.getSuit() == _currentPlayer.getTrump() && 
                 top.getSuit() != _currentPlayer.getTrump())
         {
            valid = true;
         }
         // If the suit of the card is a player's trump card, 
         // AND the top card is a trump card of a lower denomination
         else if(beatCard.getSuit() == _currentPlayer.getTrump() && 
                 beatCard.getDenomination() > top.getDenomination())
         {
            valid = true;
         }
      }
      
      return valid;
   }
   
   
   /**
    * Attempts to play the cards that are passed in
    * 
    * precondition: cardsToPlay is an array of two non-null Card objects
    */
   public boolean playCards(Card[] cardsToPlay)
   {
       boolean valid = false;
       
       // Checks if the beating card is valid.
       if (beatsTop(cardsToPlay[0]))
       {
          valid = true;
       }
       
       // Only if the move is valid do the cards get placed on the deck
       if (valid)
       {
           // Play the stored cards on the deck and remove it 
           // from the current player's hand.
           for(Card card : cardsToPlay)
           {
               _deck.add(card);
               _currentPlayer.remove(card);
           }
           
           // Advance to the next player who has cards.
           do
           {
              getNextPlayer();
              
           } while(!playerHasCards());
       }
       
       return valid;
   }
   
   
   /**
    * Plays a single card from the hand to the deck when the deck is empty
    * 
    * @param singleCard
    * @return The success of the move
    */
   public boolean playSingleCard(Card singleCard)
   {
      boolean valid = false;
      
      if(beatsTop(singleCard))
      {
         valid = true;
         _deck.add(singleCard);
         _currentPlayer.remove(singleCard);
         
         // Advance to the next player who has cards.
         do
         {
            getNextPlayer();
            
         } while(!playerHasCards());
      }
      
      return valid;
   }
   
   
   /**
    * Draws cards, adds it into the player's hand
    * 
    * @return if the move was valid (cards in the deck)
    */
   public boolean drawCards()
   {  
       boolean valid = true;
       
       if (_deck.isEmpty())
       {
           valid = false;
       }
       else
       {
           // When a player draws cards, they are added to the
           // player's hand.
           ArrayList<Card> drawnCards = _deck.draw(_currentPlayer.getTrump());
           
           // Add each card to the current player's hand.
           for(Card card : drawnCards)
           {
              _currentPlayer.add(card);
           }
           
           // Advance to the next player who has cards.
           do
           {
              getNextPlayer();
              
           } while(!playerHasCards());
       }      
       
       return valid;
   }
   
   
   /**
    * Determines whether the current player is a human player.
    * 
    * @return True if human, false if AI
    */
   public boolean isPlayerHuman()
   {
      return _currentPlayer.getType() == 1;
   }
   
   
   /**
    * Gets the current player.
    * 
    * @return The current player.
    */
   public Player getCurrentPlayer()
   {
      return _currentPlayer;
   }
   
   
   /**
    * Gets the next player and sets the current player to that next player
    */
   public void getNextPlayer()
   {
      // move the current player to the end of the queue
      // peek at who's at the front of the queue -> this is the next player
      _players.remove();
      _players.add(_currentPlayer);
      _currentPlayer = _players.peek();
   }
   
   
   /**
    * Determines whether the current player has cards to play with.
    * 
    * @return True if the current player has cards.
    */
   public boolean playerHasCards()
   {
      return !_currentPlayer.isEmpty();
   }
   
   
   /**
    * Get the players returned as an array of Player objects
    * 
    * @return The players as an array of Player objects
    */
   public Player[] getPlayersAsArray()
   {
      Player[] players = new Player[4];
      int i = 0;
      
      // for each of the players, add them to the array
      for(Player player : _players)
      {
         players[i] = player;
         i++;
      }
      
      return players;
   }
   
   
   /**
    * Returns the player object of the player with the
    * requested player number
    * 
    * @param playerNum
    * @return the player
    */
   public Player getPlayer(int playerNum)
   {
      Player[] players = getPlayersAsArray();
      int i = 0;
      
      while(players[i].getPlayerNum() != playerNum)
      {
         i++;
      }
      
      return players[i];
   }
   
   
   /**
    * Determines whether the current game is over.
    * 
    * @return True if game over.
    */
   public boolean isGameOver()
   {
      boolean over = false;
      int emptyHands = 0;
      
      // The game is over when all players have no cards left. If a player is
      // found with cards, the game is not yet over.
      for(Player player : _players)
      {
         if(player.isEmpty())
         {
            emptyHands++;
            
            if(!_winOrder.contains(player))
            {
               _winOrder.add(player);
            }
         }
      }
      
      if(emptyHands >= 3)
      {
         over = true;
         
         for(Player player : _players)
         {
            if(player.isEmpty())
            {
               if(!_winOrder.contains(player))
               {
                  _winOrder.add(player);
               }
            }
         }
      }
      
      return over;
   }
}
